package com.hmdp.utils;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 
 * @author jie
 * @version 1.0 2025-01-01
 */
@Component
public class CacheClient {
    private static final String NULL_VALUE = "NULL_VALUE";
    private static final Long NULL_VALUE_EXPIRE_TIME = 60L;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public <T> void set(String key, T value, long expireTime, TimeUnit timeUnit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expireTime)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public <T> T get(String key, Class<T> type, DbCallback<T> dbCallback, long expireTime, TimeUnit timeUnit) {
        String json = stringRedisTemplate.opsForValue().get(key);
        if (NULL_VALUE.equals(json)) {
            return null;
        }
        if (json == null) {
            boolean isLock = tryLock(key);
            if (!isLock) {
                try {
                    Thread.sleep(100);
                    return get(key, type, dbCallback, expireTime, timeUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            try {
                json = stringRedisTemplate.opsForValue().get(key);
                if (json != null) {
                    RedisData redisData = JSONUtil.toBean(json, RedisData.class);
                    T data = JSONUtil.toBean((JSONObject) redisData.getData(), type);
                    if (redisData.getExpireTime().isBefore(LocalDateTime.now())) {
                        buildCache(key, dbCallback, LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expireTime)));
                    }
                    return data;
                }
                return buildCache(key, dbCallback, LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expireTime)));
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unlock(key);
            }
        }

        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        T data = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        if (redisData.getExpireTime().isBefore(LocalDateTime.now())) {

        }
        return data;
    }

    /**
     * 缓存重建
     * @param key
     * @param dbCallback
     * @param <T>
     */
    private <T> T buildCache(String key, DbCallback<T> dbCallback, LocalDateTime expireTime) {
        T data = dbCallback.execute();
        if (data == null) {
            stringRedisTemplate.opsForValue().set(key, NULL_VALUE, NULL_VALUE_EXPIRE_TIME, TimeUnit.SECONDS);
        } else {
            RedisData redisData = new RedisData();
            redisData.setData(data);
            redisData.setExpireTime(expireTime);
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
        }
        return data;
    }

    private boolean tryLock(String key) {
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent("lock:" + key, "lock");
        return Boolean.TRUE.equals(isLock);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete("lock:" + key);
    }

    public interface DbCallback<T> {
        T execute();
    }

    @Getter
    @Setter
    private static class RedisData {
        private LocalDateTime expireTime;
        private Object data;
    }
}
